昨天的 Day 9,我們學會了使用 @PostMapping 和 @RequestBody,讓 Spring Boot 可以直接接收 JSON 格式的資料。
例如使用者傳送:
{
"height": 162,
"weight": 50
}
Spring Boot 就能將 JSON 轉換成 Java 物件,再進行 BMI 計算。
不過在整理昨天的程式時,我發現一個問題:
目前接收資料、檢查資料、計算 BMI 和回傳結果,全部都寫在 Controller 裡面。
現在只有 BMI 一個功能,程式看起來還算簡單。
但如果之後還要加入睡眠紀錄、飲水量、運動時間,甚至 AI 功能,Controller 可能會變得越來越長。
所以今天決定先不增加新的健康功能,而是整理目前的程式架構!
今天的目標是:
認識 Controller 與 Service 的分工,並將 BMI 計算邏輯移到 Service。
回顧昨天的程式:
@PostMapping("/health/bmi")
public Map<String, Object> calculateBMI(
@RequestBody HealthRequest request) {
double height = request.getHeight();
double weight = request.getWeight();
double heightInMeters = height / 100;
double bmi = weight / (heightInMeters * heightInMeters);
// 整理並回傳結果
}
這樣寫其實可以正常運作。
但是仔細觀察,Controller 同時負責了很多事情:
如果未來功能越來越多,就可能讓 Controller 變得很難維護。
因此我們可以把不同工作交給不同的 Class。
例如:
Controller
↓
Service
↓
計算結果
↓
Controller
↓
Response
簡單來說:
Controller 負責接收請求與回傳結果,Service 負責處理主要的業務邏輯。
前幾天已經使用過:
@RestController
Controller 是 Spring Boot Web 應用程式中,負責處理 HTTP Request 的元件。
例如:
@PostMapping("/health/bmi")
當使用者發送:
POST /health/bmi
Spring Boot 就會找到對應的 Controller Method。
Controller 主要負責:
可以把 Controller 想成系統的入口。
它知道使用者想做什麼,但不一定需要自己完成所有工作。
Service 通常用來放置系統的業務邏輯。
例如我們的智慧健康助理,未來可能需要:
這些工作就可以依照功能放進不同的 Service。
例如:
BmiService
SleepService
WaterService
AiService
這樣每個 Service 都有自己的工作。
對目前的專案來說,我們先建立:
BmiService.java
讓它專門負責 BMI 計算。
在 Spring Boot 中,可以使用:
@Service
標記 Service 類別。
例如:
import org.springframework.stereotype.Service;
@Service
public class BmiService {
}
@Service 是 Spring 的一種 Annotation。
它可以讓 Spring 在元件掃描時辨識這個類別,並將它註冊成由 Spring 管理的 Bean。
這樣其他 Spring 元件就可以透過依賴注入使用它。
目前可以先理解成:
@Service 告訴 Spring,這是一個負責處理業務邏輯的元件。
接下來正式開始整理程式。
首先建立:
BmiService.java
程式如下:
import org.springframework.stereotype.Service;
@Service
public class BmiService {
public double calculateBMI(double height, double weight) {
if (!Double.isFinite(height) ||
!Double.isFinite(weight) ||
height <= 0 || weight <= 0) {
throw new IllegalArgumentException(
"Height and weight must be positive numbers."
);
}
double heightInMeters = height / 100;
double bmi = weight / (heightInMeters * heightInMeters);
if (!Double.isFinite(bmi)) {
throw new IllegalArgumentException(
"Unable to calculate BMI."
);
}
return Math.round(bmi * 100.0) / 100.0;
}
}
這段程式其實就是把昨天寫在 Controller 裡面的 BMI 計算搬過來。
最大的差別是:
現在 BmiService 只需要負責計算 BMI,不需要知道 HTTP Request 是怎麼傳進來的。
例如:
calculateBMI(162, 50);
就可以得到:
19.05
這樣未來即使不是透過網頁呼叫,也可以重複使用這個計算方法。
接下來回到:
HealthController.java
原本 Controller 自己計算 BMI。
今天要改成呼叫 BmiService。
完整程式如下:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
public class HealthController {
private final BmiService bmiService;
public HealthController(BmiService bmiService) {
this.bmiService = bmiService;
}
@PostMapping("/health/bmi")
public ResponseEntity<Map<String, Object>> calculateBMI(
@RequestBody HealthRequest request) {
Map<String, Object> result = new HashMap<>();
try {
double height = request.getHeight();
double weight = request.getWeight();
double bmi = bmiService.calculateBMI(height, weight);
result.put("height", height);
result.put("weight", weight);
result.put("bmi", bmi);
return ResponseEntity.ok(result);
} catch (IllegalArgumentException e) {
result.put("error", e.getMessage());
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(result);
}
}
}
這次的 Controller 比昨天更有條理。
它不需要自己寫 BMI 公式,而是透過:
bmiService.calculateBMI(height, weight);
呼叫 Service。
整個流程變成:
使用者傳送 JSON
↓
HealthController
↓
取得身高與體重
↓
呼叫 BmiService
↓
BmiService 計算 BMI
↓
回傳計算結果
↓
HealthController
↓
回傳 JSON
剛剛的 Controller 出現了一段新的程式:
private final BmiService bmiService;
public HealthController(BmiService bmiService) {
this.bmiService = bmiService;
}
這裡使用的是 Dependency Injection(依賴注入)。
簡單來說,就是讓 Spring 幫我們提供需要使用的物件。
因為 BmiService 已經加上:
@Service
所以 Spring 可以管理這個 Service。
當 Spring 建立 HealthController 時,就可以將 BmiService 傳入建構子。
這樣我們就不需要自己寫:
BmiService bmiService = new BmiService();
而是交給 Spring 管理物件之間的依賴關係。
這種方式稱為 Constructor Injection(建構子注入)。
它也是 Spring 開發中很常見的做法。
今天還多了一個新的東西:
ResponseEntity
前幾天我們直接回傳:
Map<String, Object>
Spring Boot 就會將它轉換成 JSON。
但今天改成:
ResponseEntity<Map<String, Object>>
原因是我們希望除了回傳資料,也能控制 HTTP Status Code。
例如成功時:
return ResponseEntity.ok(result);
代表回傳:
HTTP 200 OK
如果資料不合理:
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(result);
就會回傳:
HTTP 400 Bad Request
這樣 API 不只可以告訴使用者結果,也能透過 HTTP 狀態碼表示請求是否成功。
接下來重新啟動 Spring Boot。
使用 Postman 發送:
POST http://localhost:8080/health/bmi
Body 選擇:
raw → JSON
輸入:
{
"height": 162,
"weight": 50
}
預期回傳:
{
"height": 162.0,
"weight": 50.0,
"bmi": 19.05
}
HTTP Status:
200 OK
接著測試錯誤資料:
{
"height": 0,
"weight": 50
}
預期回傳:
{
"error": "Height and weight must be positive numbers."
}
HTTP Status:
400 Bad Request
這樣就代表 Controller 和 Service 已經可以互相合作。
經過今天的調整,專案可以整理成以下結構:
health-assistant
│
├── src
│ └── main
│ ├── java
│ │ └── com.example.healthassistant
│ │ │
│ │ ├── HealthAssistantApplication.java
│ │ │
│ │ ├── controller
│ │ │ └── HealthController.java
│ │ │
│ │ ├── service
│ │ │ └── BmiService.java
│ │ │
│ │ └── dto
│ │ └── HealthRequest.java
│ │
│ └── resources
│ └── application.properties
│
└── pom.xml
這裡也順便將不同用途的 Class 放進不同的 Package。
例如:
controller
放置處理 HTTP Request 的 Controller。
service
放置處理業務邏輯的 Service。
dto
放置用來傳遞資料的物件。
其中 DTO 是 Data Transfer Object 的縮寫。
昨天建立的 HealthRequest 就可以放在這裡。
實際建立檔案時,記得讓每個 Java 檔案的 package 宣告與所在位置一致,並補上需要的 import。
另外,這些 Package 應該放在 Spring Boot 主程式所在的根 Package 之下,讓 Spring 能夠正常掃描到 Controller 與 Service。
今天雖然沒有增加新的健康功能,但程式結構變得更清楚了。
以前:
Controller
├── 接收資料
├── 檢查資料
├── 計算 BMI
└── 回傳結果
現在:
Controller
├── 接收資料
├── 呼叫 Service
└── 回傳結果
Service
├── 檢查計算資料
└── 計算 BMI
每個 Class 的工作比較明確。
如果未來 BMI 計算邏輯需要修改,就可以直接修改:
BmiService.java
不一定需要更動 Controller。
因為 BMI 計算已經獨立出來,所以未來可以直接針對:
calculateBMI()
撰寫單元測試。
不需要每次都透過 Postman 才能測試計算邏輯。
未來加入更多功能時,可以建立:
SleepService
WaterService
ExerciseService
AiService
讓不同功能各自管理自己的邏輯。
這樣比較不容易把整個專案寫成一個非常大的 Controller。
雖然今天已經將 Controller 和 Service 分開,但目前的架構還不是最完整的版本。
例如:
HealthRequest 尚未加入完整的欄位驗證try-catch 處理錯誤這些都可以留到後面的挑戰慢慢改善。
我覺得這也是 Side Project 很有趣的地方。
一開始只求功能可以運作,但隨著學到更多技術,就可以慢慢把原本的程式整理得更好。
今天沒有增加新的健康功能,而是把原本的 BMI 計算程式重新整理。
最大的收穫是了解:
Controller 負責處理 HTTP 請求,Service 負責處理業務邏輯。
今天也認識了:
@Service:將類別註冊為 Spring 管理的 Service 元件ResponseEntity:控制 HTTP Response 與狀態碼現在我們的智慧健康助理已經從:
Controller
↓
BMI 計算
↓
Response
進化成:
Controller
↓
Service
↓
BMI 計算
↓
Controller
↓
Response
雖然看起來只是多了一個 Service,但對後續開發來說,這是一個很重要的改變。
畢竟這次的目標不是只完成一個 BMI 計算器,而是希望一步一步打造可以擴充的智慧健康助理!
目前雖然已經有基本的錯誤判斷,但如果使用者沒有輸入身高、輸入負數,或是傳送不完整的 JSON,程式應該怎麼處理?
下一篇準備認識:
@Valid
@NotNull
@Positive
讓智慧健康助理在接收使用者資料時,可以進行更完整的檢查!